package cn.tedu.charging.order.job;

import cn.tedu.charging.common.pojo.param.GunStatusUpdateParam;
import cn.tedu.charging.order.constant.Constant;
import cn.tedu.charging.order.dao.repository.OrderRepository;
import cn.tedu.charging.order.pojo.po.ChargingBillFailPO;
import cn.tedu.charging.order.pojo.po.ChargingBillSuccessPO;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Date;
import java.util.UUID;

/**
 * 设备自检定时任务
 * 目标 尽快的发现设备故障
 * 如果没有自检任务,设备出现了故障,走死信队列,时间会很长
 * 1分钟的自检时间
 * 加快发现设备故障的时间 尽快的通知用户 充电的用户 换桩 维修的用户 维修充电桩
 *
 * 定时任务
 * 1 按照设置的固定的时间   可以触发一次,也可以触发多次
 * 2 干一些事情
 * Quartz
 * 有存储概念 默认用的RAM 内存  可以用数据库存储 来进行持久化, 我们的订单服务重启,定时任务的数据不会丢失
 * 有机器的概念 cluster  单点故障, 分布式任务 xxl-job https://www.oschina.net/p/xxl-job?hmsr=aladdin1e1
 */
@Slf4j
public class DeviceCheckJob implements Job {


    /**
     * 必须有无参的构造器        还是不太理解？？？启动时就报错，说是找不到无参构造方法来通过反射方式制作出对象？
     * 如果没有,在初始化的时候  newInstance,获取构造器的方法是 getConstructor0 获取构造器进行 初始化 所谓的初始化就是调用构造器
     * NoSuchMethodException 找不到方法异常 (找不到默认构造器这个方法)
     */
    public DeviceCheckJob() {
    }

    /**
     * 自检任务的构造器 创建任务,设置时间
     * 入参
     * 订单信息 订单号
     * 设备信息 枪id
     * @param orderNo
     * @param gunId
     */
    public DeviceCheckJob(String orderNo,Integer gunId) throws Exception {
        log.debug("添加设备自检定时任务:订单号-{},枪id-{}",orderNo,gunId);
        //创建 调度器工厂
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        //通过调度器工厂获取调度器      又特么把异常抛在方法上
        Scheduler scheduler = schedulerFactory.getScheduler();

        //创建定时任务    固定API接口的写法
        JobDetail jobDetail = JobBuilder.newJob(DeviceCheckJob.class)
                //定义定时任务的名称和组
                .withIdentity("DeviceCheckJob" + UUID.randomUUID(),"device_check_job")
                //传入订单号     在这儿加入订单号、枪ID，是为了下面的execute方法通过context获取到这俩数据
                .usingJobData("orderNo",orderNo)
                //传入枪id
                .usingJobData("gunId",gunId)
                .build();

        //设备自检需要1分钟
        long deviceCheckTime = 1 * 60 * 1000;
        //当前时间
        long now = System.currentTimeMillis();
        //任务触发的时间   trigger扳机,触发器
        Date triggerTime = new Date(now + deviceCheckTime);
        //创建触发器 并设置时间
        Trigger trigger = TriggerBuilder.newTrigger().startAt(triggerTime).build();

        //调度器 把 任务和触发器  绑定
        scheduler.scheduleJob(jobDetail,trigger);
        //启动调度器
        scheduler.start();
    }

    /**
     * execute执行 干一些事情  实现Job的重写方法
     * @param context
     * @throws JobExecutionException
     */
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.debug("设备自检任务开始执行:{}",context);
        //todo 思考给了设备一分钟的时间来自检 一分钟以后,代码会执行到这里, 我们后续的业务该怎么做?

        //startCharging     在创建订单的第1步生成订单号之后就开始自检，自检与创建订单的后续是异步的，所以要考虑下面情况：
        //1  设备收到了充电指令,返回了开始充电成功指令 ,成功订单表有数据
        //2  设备收到了充电指令,返回了开始充电失败指令 ,失败订单表有数据
        //3  订单服务没有收到设备返回的任何信息, 成功订单表没有数据,失败订单表没有数据  设备无响应

        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        //查看JobExecutionContext源码的源码，JobExecutionContext是Job中的一种对象，有个getJobDetail()方法，这里传进来这个类，然后调用方法
        //getJobDetail()方法，返回一个JobDetail对象，它包含了关于当前Job的详细信息。getJobDataMap()再获取上面写的usingJobData以Map形式储存的数据
        //getJobDetail().getJobDataMap()：返回一个JobDataMap对象，它是一个键值对集合，可以用来在Job执行前后传递数据
        log.debug("获取定时任务上下文数据:{}",jobDataMap); //这儿打印日志是个对象的地址，而非内容

        String orderNo = jobDataMap.getString("orderNo");
        int gunId = jobDataMap.getInt("gunId");

        //OrderRepository序列化，将数据存到数据库，当订单服务能重新启动时，从数据库中回复订单信息
        //之前都是用spring直接注入bean对象，这里采用新方式，spring提供了一个接口，直接获取容器里的bean对象
        // 所以其实可以直接注入OrderRepositoryImpl的吧？？？似乎不行，因为这个类没有spring注解，应该不能注入spring对象
        //于是新建SpringContextJobUtils类，位置暂时放在job包下
        //通过SpringContextJobUtils获取到容器里的OrderRepository对象   这里是要保存数据，所以要OrderRepository对象？
        OrderRepository orderRepository =
                SpringContextJobUtils.getBean("orderRepository", OrderRepository.class);
        //如果获取失败，取到的就是空，下面调用方法就该报空指针异常，容器里没有这个名字的bean对象
        //所以选择给OrderRepositoryImpl注解改为@Repository("orderRepository")，指明容器中的对象名字，
        // 这样在找接口OrderRepository类型的对象时会扫描到它的实现类，选其中叫"orderRepository"的实现类，获取它的对象
        //也可以用名字“OrderRepositoryImpl”，注解不改名，但是编程原则是 面向接口编程，防止以后接口的实现类变多了混乱？
        //讲的什么鬼？？？那有多个实现类，其注解还叫"orderRepository1""orderRepository2"？？是得不同名字，反正名字随便起

//        //判断订单成功/失败表有没有数据
//        ChargingBillSuccessPO chargingBillSuccessPO = orderRepository.getSuccessOrder(orderNo);
//        ChargingBillFailPO chargingBillFailPO = orderRepository.getFailOrder(orderNo);
//

        //从容器里获取DeviceClient todo 自己实现
        //9.3git
        //通过订单号获取成功订单数据     细节，搜出来的叫 db...  要和下面new出来的名字区分
        ChargingBillSuccessPO dbChargingBillSuccessPO = orderRepository.getSuccessOrder(orderNo);
        //判断订单成功表有没有数据
        if (dbChargingBillSuccessPO == null) {
            //没有成功订单数据  通过订单号获取失败订单数据   数据库资源很珍贵，所以别瞎搜数据库，写在条件里面，减少资源消耗
            ChargingBillFailPO dbChargingBillFailPO = orderRepository.getFailOrder(orderNo);
            //判断订单失败表有没有数据
            if (dbChargingBillFailPO == null) {
                //没有失败订单数据  插入一条失败订单数据
                ChargingBillFailPO chargingBillFailPO = new ChargingBillFailPO();
                chargingBillFailPO.setBillId(orderNo);
                chargingBillFailPO.setFailDesc("设备自检失败-无响应");
                log.debug("设备自检失败,保存失败订单记录:{}",chargingBillFailPO);
                orderRepository.saveFailOrder(chargingBillFailPO);
                log.debug("设备自检失败,保存失败订单记录:{} 成功.",chargingBillFailPO);

                //修改设备状态“FREE”变为 故障  通知维修人员
                log.debug("修改设备状态为故障:枪id:{},状态:{}",gunId, Constant.GUN_STATS_ERROR);
                GunStatusUpdateParam updateParam = new GunStatusUpdateParam();
                updateParam.setStatus(Constant.GUN_STATS_ERROR);
                updateParam.setId(gunId);
//                deviceClient.updateGunStatus(updateParam);
                log.debug("修改设备状态为故障:枪id:{},状态:{} 成功",gunId,Constant.GUN_STATS_ERROR);
                //通知设备维修人员 todo
                log.debug("通知用户设备故障,请您更换充电设备,给您带来的不便,敬请谅解..",gunId,Constant.GUN_STATS_ERROR);
                log.debug("调用 营销系统(优惠券服务)给用户发送优惠券 1张价值100的优惠已经发到你的账户请注意查收...");
                log.debug("调用 保障服务 成功,通知维修人员对设备进行检查和维护",gunId,Constant.GUN_STATS_ERROR);
                // 第1步 创建保障服务
                // 第2步 和保障服务通信，有下面2种实现方式
                //  2.1 通过open-feign 直接调用
                //  2.2 通过rabbit-mq 给 保障服务发送消息 ,保障服务接收到消息,通知维修人员,维修人员接收到消息,进行设备检查维修
                //业务扩展 到家服务
            }else {
                //如果有失败的订单记录, 设备失败原因是  "设备自检失败-设备有响应但是充不了电"     为啥不改？因为反正都是坏的？
                log.debug("设备自检失败-设备有响应但是充不了电,这里的逻辑无需修改 设备失败原因 ");
            }
        }else {
            log.debug("有订单成功的记录.设备自检成功,有响应,能充电");
            //把用户id,枪id,运营商id,车辆id,充电站id 更新到订单表，但不是必须的，想更新就更新呗
            log.debug("更新设备信息到订单表,这里不是必须的，就是演示 更新枪id:{}到成功订单表,更新条件:订单号:{}",gunId,orderNo);
            orderRepository.updateOrderDeviceInfo(orderNo,gunId);
            log.debug("更新设备信息到订单表,这里不是必须的，就是演示 更新枪id:{}到成功订单表 成功!!!,更新条件:订单号:{}",gunId,orderNo);
        }
    }

}
